// ========================================================================
// Copyright 2012 leolee<workspaceleo@gmail.com>
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//     http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
package org.jcommon.com.util.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.log4j.Logger;


/**
 * @author leoLee leo.li@protel.com.hk
 */
public class HttpReqeust implements Runnable{
	private static Logger logger = Logger.getLogger(HttpReqeust.class);
	
	public static final String  GET = "GET";
	public static final String  POST = "POST";
	
	private String url_;
	private String content_;
	private String charset_; 
	private String method_;
	private HttpListener listener_;
	private String passphrase = "changeit";//"password.com";
	
	private boolean trusted = false;
	
	public HttpReqeust(String url, String content, String charset, String method, HttpListener listener){
		this.url_      = url;
		this.charset_  = charset;
		this.content_  = content;
		this.method_   = method;
		this.listener_ = listener;
	}
	
	public HttpReqeust(String url, String content, String method, HttpListener listener){
		this(url,content,"utf-8",method,listener);
	}
	
	public HttpReqeust(String url, String content, String method, HttpListener listener, boolean trusted){
		this(url,content,"utf-8",method,listener);
		this.trusted = trusted;
	}
	
	public HttpReqeust(String url, String charset, HttpListener listener){
		this(url,null,charset,GET,listener);
	}
	
	public HttpReqeust(String url, HttpListener listener){
		this(url,null,"utf-8",GET,listener);
	}
	
	public HttpReqeust(String url, HttpListener listener, boolean trusted){
		this(url,null,"utf-8",GET,listener);
		this.trusted = trusted;
	}
	
	StringBuilder sResult = new StringBuilder();
	public void run() {
		// TODO Auto-generated method stub
		String charsetName = charset_;
		URL url = null;
		HttpURLConnection httpConnection = null;
		InputStream httpIS = null;
		java.io.BufferedReader http_reader = null;
		try {
			url = new URL(url_);
			//httpConnection = (HttpURLConnection) url.openConnection(proxy);
			if(url_!=null && url_.startsWith("https") && !trusted){
				SSLSocketFactory factory = null;
				try {
					factory = verifyCert();
				} catch (Exception e) {
					// TODO Auto-generated catch block
					logger.error("verifyCert:", e);
		            if(listener_!=null)listener_.onException(this, e);
		            return;
				}
				httpConnection = (HttpsURLConnection) url.openConnection();
				if(factory!=null)
					((HttpsURLConnection) httpConnection).setSSLSocketFactory(factory);
				((HttpsURLConnection) httpConnection).setHostnameVerifier(new NullHostnameVerifier());
			}
			else
				httpConnection = (HttpURLConnection) url.openConnection();
			httpConnection.setConnectTimeout(10000);
			httpConnection.setReadTimeout(10000);
			httpConnection.setRequestMethod(method_);
			if(GET.equalsIgnoreCase(method_)){
				httpConnection.setRequestProperty("Accept", "*/*");
			}else if(POST.equalsIgnoreCase(method_)){
				httpConnection.setDoOutput(true);
				httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
				httpConnection.setRequestProperty("Content-Length", String.valueOf(content_.getBytes().length));
				
				PrintWriter out = null;
				out = new PrintWriter(new OutputStreamWriter(httpConnection.getOutputStream(), charsetName));
				out.print(content_);
				out.flush();
				out.close();
			}
			int responseCode = httpConnection.getResponseCode();
			if (responseCode == 200) {
				httpIS = httpConnection.getInputStream();
				http_reader = new java.io.BufferedReader(new java.io.InputStreamReader(httpIS, charsetName));
				String line = null;
				while ((line = http_reader.readLine()) != null) {
					sResult.append(line);
				}
				if(listener_!=null)listener_.onSuccessful(this, sResult);
			}else if (responseCode >= 400) {
				httpIS = httpConnection.getErrorStream();
				http_reader = new java.io.BufferedReader(new java.io.InputStreamReader(httpIS, charsetName));
				String line = null;
				while ((line = http_reader.readLine()) != null) {
					sResult.append(line);
				}
				logger.info("[URL][response][failure]" + sResult);
				if(listener_!=null)listener_.onFailure(this, sResult);
			}
			else {
				sResult.append("[URL][response][failure][code : " + responseCode + " ]");
				if(listener_!=null)listener_.onFailure(this, sResult);
				logger.info("[URL][response][failure][code : " + responseCode + " ]");
			}
		}catch(java.net.SocketTimeoutException e){
			if(listener_!=null)listener_.onTimeout(this);
			logger.warn("[HttpReqeust]TimeOut:"+url_);			
		} 
		catch (IOException e) {
			if(listener_!=null)listener_.onException(this, e);
			logger.warn("[HttpReqeust] error:"+url_ +"\n"+e);	
		}
		finally {
			try {
				if (http_reader != null)
					http_reader.close();
				if (httpIS != null)
					httpIS.close();
				if (httpConnection != null)
					httpConnection.disconnect();
			} catch (IOException e) {
				logger.error("[HttpReqeust]finally error:"+url_ +"\n"+e);
				if(listener_!=null)listener_.onException(this, e);
			}
		}
	}
	
	public SSLSocketFactory verifyCert() throws Exception{
		if(url_!=null && url_.startsWith("https")){
			String host = url_.substring(url_.indexOf("/", 7)+1, url_.indexOf("/", 9));
			String[] c = host.split(":");
		    host = c[0];
		    int port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
		    char[] passphrase = this.passphrase.toCharArray();
		    
		    char SEP = File.separatorChar;
		    File dir = new File(System.getProperty("java.home") + SEP
			    + "lib" + SEP + "security");
		    File file = new File(dir, "cacerts");
//		    File dir = new File(System.getProperty("jetty.home") + SEP
//				    + "etc");
//		    File file = new File(dir, "keystore");
		    
			logger.info("Loading KeyStore " + file + "...");
			InputStream in = new FileInputStream(file);
			KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
			ks.load(in, passphrase);
			in.close();

			SSLContext context = SSLContext.getInstance("TLS");
			TrustManagerFactory tmf =
			    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
			tmf.init(ks);
			X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
			SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
			context.init(null, new TrustManager[] {tm}, null);
			SSLSocketFactory factory = context.getSocketFactory();
			logger.info("factory:"+factory);
			logger.info("Opening connection to " + host + ":" + port + "...");
			SSLSocket socket = (SSLSocket)factory.createSocket(host, port);
			socket.setSoTimeout(10000);
			try {
				logger.info("Starting SSL handshake...");
			    socket.startHandshake();
			    socket.close();
			    logger.info("certificate is already trusted");
			    return factory;
			} catch (SSLException e) {
				logger.warn("no certificate trusted");
			}

			X509Certificate[] chain = tm.chain;
			if (chain == null) {
				logger.warn("Could not obtain server certificate chain");
			    return null;
			}

			logger.info("Server sent " + chain.length + " certificate(s):");
			MessageDigest sha1 = MessageDigest.getInstance("SHA1");
			MessageDigest md5 = MessageDigest.getInstance("MD5");
			for (int i = 0; i < chain.length; i++) {
			    X509Certificate cert = chain[i];
			    logger.info
			    	(" " + (i + 1) + " Subject " + cert.getSubjectDN());
			    logger.info("   Issuer  " + cert.getIssuerDN());
			    sha1.update(cert.getEncoded());
			    logger.info("   sha1    " + toHexString(sha1.digest()));
			    md5.update(cert.getEncoded());
			    logger.info("   md5     " + toHexString(md5.digest()));
			}

			logger.info("certificate to add to trusted keystore");


			X509Certificate cert = chain[0];
			String alias = host + "-" + (0 + 1);
			ks.setCertificateEntry(alias, cert);

			OutputStream out = new FileOutputStream(file);
			ks.store(out, passphrase);
			out.close();
			
			logger.info(cert);
			logger.info("Added certificate to keystore using alias '" + alias + "'");
			return factory;
		}
		return null;
	}

	public void setPassphrase(String passphrase) {
		this.passphrase = passphrase;
	}

	public String getPassphrase() {
		return passphrase;
	}
	
    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

	    private static String toHexString(byte[] bytes) {
		StringBuilder sb = new StringBuilder(bytes.length * 3);
		for (int b : bytes) {
		    b &= 0xff;
		    sb.append(HEXDIGITS[b >> 4]);
		    sb.append(HEXDIGITS[b & 15]);
		    sb.append(' ');
		}
		return sb.toString();
    }
    
	private static class SavingTrustManager implements X509TrustManager {

		private final X509TrustManager tm;
		private X509Certificate[] chain;

		SavingTrustManager(X509TrustManager tm) {
		    this.tm = tm;
		}

		public X509Certificate[] getAcceptedIssuers() {
		    throw new UnsupportedOperationException();
		}

		public void checkClientTrusted(X509Certificate[] chain, String authType)
			throws CertificateException {
		    throw new UnsupportedOperationException();
		}

		public void checkServerTrusted(X509Certificate[] chain, String authType)
			throws CertificateException {
		    this.chain = chain;
		    tm.checkServerTrusted(chain, authType);
		}
	}
	
	private static class NullHostnameVerifier implements HostnameVerifier {
	    public boolean verify(String hostname, SSLSession session) {
	        return true;
	    }
	}
	
	public String getResult(){
		return sResult.toString();
	}
}
